package se.liu.oschi129.game.objects;

import java.awt.Shape;
import java.awt.geom.Rectangle2D;

import se.liu.johfa428.levelstate.LevelState.State;
import se.liu.oschi129.animation.set.CharacterSet;
import se.liu.oschi129.game.objects.items.CollectibleObject;
import se.liu.oschi129.game.objects.enemies.*;
import se.liu.oschi129.game.setspace.*;
import se.liu.oschi129.shapefactory.ShapeFactory;
import se.liu.oschi129.world.interfaces.*;
import se.liu.oschi129.world.model.World;
import se.liu.oschi129.world.model.WorldInput;
import se.liu.oschi129.world.objects.*;

/**
 * This is the player object.
 * 
 * @author oschi129
 */
public class ObjectPlayer extends WorldMovableCharacterObject implements InputAction {

	// Define the directions
	private enum Direction {LEFT, NONE, RIGHT};
	
	// The current and last used direction (last direction cannot be none)
	private Direction direction, lastDirection = Direction.RIGHT;
	
	// This variable is true if the player is crouching, otherwise false
	private boolean crouch = false, jump = false;
	
	// Default walk/run configuration of the player object
	private double walkSpeed = 4, runSpeed = 6, jumpSpeed = 11, acceleration = 1;
	
	/** Current speed and maximum speed of the object. Since we are not using
	 *  the linear nor quadratic patterns for the horizontal movement we cannot
	 *  use the default implementations in WorldMovableObject to this object.  
	 */
	private double speed = 0, maxSpeed = walkSpeed;
	
	// The cooldown is set when you are hit - if cooldown > 0 then you cannot take any damage
	private int damageCooldown = 0;
	
	// The state of the player (0 = normal, 1 = spike hat)
	private int state;
	
	/**
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 * - - - - - - - - - - - - Constructors - - - - - - - - - - - 
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 */
	
	public ObjectPlayer(double x, double y) {
		this(x, y, new AnimatedSetGoombaSmall());
	}
	
	public ObjectPlayer(double x, double y, CharacterSet characterSet) {
		super(Type.SOFT, characterSet, true, x,y,24,32);
		setAnimation(getAnimationSet().getMoveRightAnimation());
		setImageSpeed(0);
		setMaskFactory(new DefaultMask());
		setDirection(Direction.NONE);
		setDepth(-1);
		setState(0);
	}
	
	/**
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 * - - - - - - - - - - - Public methods - - - - - - - - - - 
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 */
	
	/** Sets the horizontal acceleration of the player object */
	public void setAcceleration(double acceleration) {
		this.acceleration = acceleration;
	}
	
	/** Returns the horizontal acceleration of the player object */
	public double getAcceleration() {
		return acceleration;
	}
	
	/** Sets the walking speed of the player object */
	public void setWalkSpeed(double walkSpeed) {
		this.walkSpeed = walkSpeed;
	}
	
	/** Returns the walking speed of the player object */
	public double getWalkSpeed() {
		return walkSpeed;
	}
	
	/** Sets the running speed of the player object */
	public void setRunSpeed(double runSpeed) {
		this.runSpeed = runSpeed;
	}
	
	/** Returns the running speed of the player object */
	public double getRunSpeed() {
		return runSpeed;
	}
	
	/** Sets the jumping speed of the player object */
	public void setJumpSpeed(double jumpSpeed) {
		this.jumpSpeed = jumpSpeed;
	}
	
	/** Returns the jumping speed of the player object */
	public double getJumpSpeed() {
		return jumpSpeed;
	}
	
	/** Sets the state of the object */
	public void setState(int state) {
		this.state = state;
		switch (state) {
		case 0: setAnimationSet(new AnimatedSetGoombaSmall()); break;
		case 1: setAnimationSet(new AnimatedSetGoombaNormal()); break;
		case 2: setAnimationSet(new AnimatedSetGoombaSpike()); break;
		}
	}
	
	/** Returns the state of the object */
	public int getState() {
		return state;
	}
	
	/** This action is performed when the user presses a key - inherited  from the KeyboardAction interface. */
	public void performInputAction(WorldInput input) {
		// move left
		if (input.leftDown()) { setDirection(Direction.LEFT); }
		// move right
		else if (input.rightDown()) {setDirection(Direction.RIGHT); }
		// start jumping
		else if (input.jumpDown()) { if (!getCrouch()) setJump(true); }
		// activate running
		else if (input.runDown()) { setMaxSpeed(getRunSpeed()); }
		// start crouching
		else if (input.crouchDown()) { setCrouch(true); setMaskFactory(new CrouchMask()); }
		// break left
		else if (input.leftUp()) { if (getDirection() == Direction.LEFT) setDirection(Direction.NONE); }
		// break right
		else if (input.rightUp()) { if (getDirection() == Direction.RIGHT) setDirection(Direction.NONE); }
		// stop jumping
		else if (input.jumpUp()) { setJump(false); }
		// deactivate running
		else if (input.runUp()) { setMaxSpeed(getWalkSpeed()); }
		// stop crouching
		else if (input.crouchUp()) { setCrouch(false); }
	}
	
	/** The step action for the player */
	@Override public void performStepAction(World world) {
		// Perform all movements
		realizeGravity(world);
		super.performStepAction(world);
		moveCharacter(world);
		realizeFriction(world);
		stayInsideWorld(world);
		
		// Animate the character
		animateCharacter(world.placeFree(this, getX(), getY() + getVerticalSpeed() + 2));
		if (getSpeed() == 0)
			setImageSpeed(0.5);
		else
			setImageSpeed(Math.abs(getSpeed())/8);
		
		// "Heals" the player from damage - removing its invulnerability within a few steps  
		healDamage();
		
		// Check for collisions with other objects in the world
		for (WorldObject other : world.getObjects()) {
			// Move with a platform if you are standing on top of it
			if (other instanceof ObjectMovablePlatform && other.isMovable() && onTopOf(other)) {
				setX(getX() + ((WorldMovableObject) other).getHorizontalSpeed());
				setY(getY() + ((WorldMovableObject) other).getVerticalSpeed());
			} else if (collision(other)) {
				// Check for collisions with other objects
				
				// A collectible object
				if (other instanceof CollectibleObject)
					((CollectibleObject) other).performPickupAction(world, this);
				
				// A goal
				if (other instanceof ObjectGoal)	 {		
					((ObjectGoal) other).getLevelState().setState(State.CLEAR);
					world.popSynchronized();
				}
				
				// An enemy
				if (other instanceof EnemyObject && !getDamaged()) {
					if (other instanceof DefeatableEnemyObject && other.getY() > getY() && getVerticalSpeed() > 0) {
						jump(false,null);
						world.setPoints(world.getPoints() + 50);
						world.removeObjectSynchronized(other);
					} else
						setDamaged(world, true);
				}	
			}
		}
		
		// Remove (kill) the player if he is below the room or if the time runs out
		if (getY() > world.getHeight() || world.getTime() == 0)
			world.removeObjectSynchronized(this);
	}
	
	
	/** Create a dead player on destruction */
	@Override public void performDestructionAction(World world) {
		world.removeInteractiveObject(this);
		world.addObjectSynchronized(new ObjectPlayerDead(getAnimationSet().getDeathAnimation(),getX(), getY()));
	}
	
	
	 /**
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 * - - - - - - - - - - Protected methods - - - - - - - -
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 */
	
	/** This function is overridden - the default function is found in WorldMovableObject */
	@Override protected void realizeGravity(World world) {
		if (world.placeFree(this, getX(), getY() + getVerticalSpeed())) {
			// No object below - accelerate
			setVerticalAcceleration(getPhysicalRules().getGravity());
		} else {
			// There is an object below - stop!
			setVerticalAcceleration(0);
			if (getVerticalSpeed() > 0) {
				world.makeContact(this, 0, 1); // Move until a contact is made
			} else if (getVerticalSpeed() < 0) {
				world.makeContact(this, 0, -1); // Move until a contact is made
				// If you hit an object from below - perform the hitAction for the object
				for (WorldObject obj : world.getObjects()) {
					if (obj.hitFromBelow(this))
						obj.performHitAction(world);
				}
			} else {
				setX(getX() + 1); // Move right is stuck inside an object
			}
			setVerticalSpeed(0);
		}
		// Set the maximal falling speed
		if (getVerticalSpeed() > getPhysicalRules().getMaxFallSpeed()) {
			setVerticalSpeed(getPhysicalRules().getMaxFallSpeed());
			setVerticalAcceleration(0);
		}
		
	}
	
	/** This function is overridden - the default function is found in WorldMovableObject */
	@Override protected void realizeFriction(World world) {
		if (getDirection() == Direction.NONE || getCrouch()) {
			if (getSpeed() > 0)
				setSpeed(Math.max(0, getSpeed()-getPhysicalRules().getFriction()));
			else if (getSpeed() < 0) 
				setSpeed(Math.min(0, getSpeed()+getPhysicalRules().getFriction()));
		}
	}
	
	/**
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 * - - - - - - - - - - Private methods - - - - - - - - - - 
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 */
	
	/** Sets the current direction */
	private void setDirection(Direction direction) {
		this.direction = direction;
		if (direction != Direction.NONE)
			lastDirection = direction;
	}
	
	/** Returns the current direction */
	private Direction getDirection() {
		return direction;
	}
	
	/** Returns the last used direction (cannot return none) */
	private Direction getLastDirection() {
		return lastDirection;
	}
	
	/** Sets the speed of the player */
	private void setSpeed(double speed) {
		this.speed = speed;
	}
	
	/** Returns the speed of the player */
	private double getSpeed() {
		return speed;
	}
	
	/** Sets whatever the player is crouching or not */
	private void setCrouch(boolean crouch) {
		this.crouch = crouch;
	}
	
	/** Returns true if the player is crouching */
	private boolean getCrouch() {
		return crouch;
	}
	
	/** Sets whatever the player is jumping or not */
	private void setJump(boolean jump) {
		this.jump = jump;
	}
	
	/** Returns true if the player is jumping */
	private boolean getJump() {
		return jump;
	}
	
	/** Set the maximal speed for the player */
	private void setMaxSpeed(double maxSpeed) {
		this.maxSpeed = maxSpeed;
	}
	
	/** Returns the maximal speed for the player */
	private double getMaxSpeed() {
		return maxSpeed;
	}
	
	/** Selects the animation for the character */
	private void animateCharacter(boolean inAir) {
		if (inAir) {
			// Set animation - the character is in the air
			if (getLastDirection() == Direction.LEFT)
				setAnimation(getAnimationSet().getJumpLeftAnimation()); // Jumping/Falling left
			else if (getLastDirection() == Direction.RIGHT)
				setAnimation(getAnimationSet().getJumpRightAnimation()); // Jumping/Falling right
		} else {
			// Set animation - the character is not in the air
			if (getLastDirection() == Direction.LEFT) {
				if (getSpeed() > 0)
					setAnimation(getAnimationSet().getBreakRightAnimation()); // Moving left and accelerating to the right (breaking right)
				else if (getSpeed() < 0)
					setAnimation(getAnimationSet().getMoveLeftAnimation()); // Moving left
				else
					setAnimation(getAnimationSet().getStandLeftAnimation()); // Standing still facing left
			}
			else if (getLastDirection() == Direction.RIGHT) {
				if (getSpeed() < 0)
					setAnimation(getAnimationSet().getBreakLeftAnimation()); // Moving right and accelerating to the left (breaking left)
				else if (getSpeed() > 0)
					setAnimation(getAnimationSet().getMoveRightAnimation()); // Moving right
				else
					setAnimation(getAnimationSet().getStandRightAnimation()); // Standing still facing right
			}
			if (getCrouch()) {
				if (getLastDirection() == Direction.LEFT)
					setAnimation(getAnimationSet().getCrouchLeftAnimation()); // Crouching left
				else
					setAnimation(getAnimationSet().getCrouchRightAnimation()); // Crouching right
			}
		}
	}
	
	/** Moves the character in the world */
	private void moveCharacter(World world) {
		if (getJump()) {
			// The player wants to jump
			if (!world.placeFree(this, getX(), getY() + 2))
				jump(true,world); // Perform a jump
		}
		if (getDirection() == Direction.LEFT && !getCrouch()) {
			// The player wants to move left (cannot be crouching at the same time)
			if (world.placeFree(this, getX() + getSpeed() - getAcceleration(), getY())) {
				// Increase the speed (-getMaxSpeed <= speed <= getMaxSpeed)
				setSpeed(Math.max(getSpeed()-getAcceleration(), -getMaxSpeed()));
				// Move left if the place is free
				setX(getX() + getSpeed());
			} else 
				setSpeed(0); // Stop moving - the place is not free
		} else if (getDirection() == Direction.RIGHT && !getCrouch()) {
			// The player wants to move right (cannot be crouching at the same time)
			if (world.placeFree(this, getX() + getSpeed() + getAcceleration(), getY())) {
				// Increase the speed (-getMaxSpeed <= speed <= getMaxSpeed)
				setSpeed(Math.min(getSpeed()+getAcceleration(), getMaxSpeed()));
				// Move right if the place is free
				setX(getX() + getSpeed());
			} else 
				setSpeed(0); // Stop moving - the place is not free
		} else {
			if (world.placeFree(this, getX() + getSpeed(), getY()))
				setX(getX() + getSpeed()); // Move the character but use the realizeFriction to deaccelerate
			else
				setSpeed(0); // Stop moving - the place is not free
		}
	}
	
	/** Forces the player to stay inside the worlds horizontal space */
	private void stayInsideWorld(World world) {
		if (getX() < 0) {
			setX(0);
			setSpeed(0);
		} else if (getX() + getWidth() > world.getWidth()) {
			setX(world.getWidth() - getWidth());
			setSpeed(0);
		}
	}
	
	/** This function makes the character jump - if checkBelow is true then the player will check if
	 * the object is standing on something solid before jumping. */
	private void jump(boolean checkBelow, World world) {
		if (!checkBelow || !world.placeFree(this, getX(), getY() + 2))
			setVerticalSpeed(-getJumpSpeed()-Math.abs(getSpeed()/2)); // Make the character jump
	}
	
	/** If damage is set to true then the player will be invulnerable for 30 steps */
	private void setDamaged(World world, boolean damage) {
		if (damage)
			damageCooldown = 30;
		else
			damageCooldown = 0;
		switch (getState()) {
		case 0: world.removeObjectSynchronized(this);
		case 1: setState(0); break;
		case 2: setState(1); break;
		}
	}
	
	/** Returns true if the player is vulnerable */
	private boolean getDamaged() {
		return (damageCooldown > 0);
	}
	
	/** "Heals" the character -  returns the character to a vulnerable state within 30 steps */
	private void healDamage() {
		if (damageCooldown > 1) {
			damageCooldown--;
			if (damageCooldown % 4 == 0)
				setVisible(true);
			else
				setVisible(false);
		} else if (damageCooldown == 1) {
			setVisible(true);
			damageCooldown = 0;
		}
	}
	
	/**
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 * - - - - - - - - - - Private classes - - - - - - - - - - -
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	 */

	/** Defines the default mask for the player */
	private class DefaultMask implements ShapeFactory {
		
		public Shape makeShape(double x, double y, double width, double height) {
			return new Rectangle2D.Double(x+8,y+8,width-16,height-8);
		}
		
	}
	
	/** Defines the mask used when the player is crouching */
	private class CrouchMask implements ShapeFactory {
		
		public Shape makeShape(double x, double y, double width, double height) {
			return new Rectangle2D.Double(x+8,y+24,width-16,height-24);
		}
		
	}
	
}
